/*
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Library General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor Boston, MA 02110-1301,  USA
 */

/**
 * @version coin.c 0.0.1
 * @package comoro
 * @author Dr.NP <np@bsgroup.org>
 *
 * Coin server
 */

#include "bspd.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <evhttp.h>
#include <pthread.h>

#ifdef HAVE_MALLOC_H
#ifndef __OpenBSD__
#include <malloc.h>
#endif
#endif

struct http_session
{
	int session_id;
	unsigned long set_time;
	unsigned long touch_time;
	int bid;
	int blevel;
	int runner_id;
	char session_key[SESSION_KEY_LENGTH * 2];
};

extern struct conf setting;
extern struct event_base *ev_base;
struct http_session *session_list;
int session_list_size;

/* Coin server initialization */
int coin_init()
{
	if (!ev_base)
	{
		fprintf(stderr, "No event base initialized\n");
		log_add("No event base initialized");
		
		exit(EXIT_GENERAL);
	}

	struct evhttp *coin_srv = evhttp_new(ev_base);

	if (!coin_srv)
	{
		// Status server start error
		fprintf(stderr, "Status server startup failed\n");
		log_add("Status server startup failed");

		exit(EXIT_GENERAL);
	}

	// MySQL init
	if (0 != db_mysql_init() || 0 != db_mysql_create_env(0))
	{
		fprintf(stderr, "Database initialized error\n");
		exit(EXIT_DB_ERROR);
	}

	session_list = (struct http_session *) malloc(SESSION_LIST_SIZE * sizeof(struct http_session));

	if (!session_list)
	{
		fprintf(stderr, "Session list initialization failed\n");
		exit(EXIT_MEMORY_ERROR);
	}

	memset(session_list, 0, SESSION_LIST_SIZE * sizeof(struct http_session));
	session_list_size = SESSION_LIST_SIZE;

	char *srv_addr_str = setting.coin_srv_listen_addr;
	int srv_port = setting.coin_srv_port;
	unsigned long srv_addr = inet_addr(srv_addr_str);

	if (srv_addr < 0)
	{
		srv_addr = INADDR_ANY;
	}
	
	if (srv_port < 1024)
	{
		srv_port = DEFAULT_COIN_SRV_PORT;
	}

	evhttp_bind_socket(coin_srv, (const char *) srv_addr_str, (u_short) srv_port);
	evhttp_set_gencb(coin_srv, coin_request, NULL);

	queue_clock_cb_func_add(http_session_check_cb, NULL);

	fprintf(stderr, "Coin server created on port %d\n", srv_port);
	log_add("Coin server created on port %d", srv_port);
	
	return RTN_SUCCESS;
}

/* Handle HTTP request */
void coin_request(struct evhttp_request *r, void *arg)
{
	struct evbuffer *buf = evbuffer_new();
	static char query[1024];
	static char request_uri[MAX_REQUEST_URI_LENGTH];
	static char session_key[SESSION_KEY_LENGTH * 2], tmp_key[SESSION_KEY_LENGTH];
	struct mysql_row **row;
	int nc;
	int runner_id, blevel;
	long long amount, trans;

	strncpy(request_uri, evhttp_request_uri(r), MAX_REQUEST_URI_LENGTH);
	
	// Spite request
	char *token;
	const char *delimiters = "/";
	token = strtok(request_uri, delimiters);

	if (token)
	{
		if (0 == strncasecmp(token, "auth", 4))
		{
			// Login
			char *bid = strtok(NULL, delimiters);
			char *bpass = strtok(NULL, delimiters);
			char *brid = strtok(NULL, delimiters);

			if (bid && bpass && brid)
			{
				// Do login
				sprintf(query, "SELECT %s, %s FROM %s WHERE %s = '%s' AND %s = '%s' AND %s = '%s'", 
				    	setting.mysql_brid_column, 
						setting.mysql_blevel_column, 
						setting.mysql_banker_table, 
						setting.mysql_bid_column, 
						bid, 
						setting.mysql_bpass_column, 
						bpass, 
						setting.mysql_brid_column, 
				    	brid);

				if (0 == db_mysql_query(0, query))
				{
					row = db_mysql_fetch_row(0, &nc);

					if (nc == 2)
					{
						// Login successfully
						int i;
						runner_id = (row && row[0]->value) ? atoi(row[0]->value) : 0;
						blevel = (row && row[1]->value) ? atoi(row[1]->value) : 0xFF;

						if (brid > 0)
						{
							package_unique_id(tmp_key, SESSION_KEY_LENGTH);

							for (i = 0; i < SESSION_KEY_LENGTH; i ++)
							{
								sprintf(session_key + i * 2, "%02x", (unsigned char) tmp_key[i]);
							}

							http_session_add(atoi(bid), runner_id, blevel, session_key);
							evbuffer_add_printf(buf, "%s", session_key);
						}

						else
						{
							evbuffer_add_printf(buf, "DATA_ERROR");
						}
					}

					else
					{
						evbuffer_add_printf(buf, "AUTH_FAILED");
					}

					db_mysql_del_row(row, nc);
				}

				else
				{
					fprintf(stderr, "Query error:%s\n", query);
		
					return;
				}
			}
		}

		else if (SESSION_KEY_LENGTH * 2 == strlen(token))
		{
			// Available session
			int session_id = http_session_get(token);

			if (session_id >= 0)
			{
				// Auth successfully
				char *oper = strtok(NULL, delimiters);

				if (oper)
				{
					if (0 == strncasecmp(oper, "get", 3))
					{
						// Get current coin
						sprintf(query, "SELECT %s FROM %s WHERE %s = %d", 
						    	setting.mysql_bamount_column, 
						    	setting.mysql_banker_table, 
						    	setting.mysql_bid_column, 
						    	session_list[session_id].bid);

						if (0 == db_mysql_query(0, query))
						{
							row = db_mysql_fetch_row(0, &nc);

							if (nc == 1)
							{
								amount = (row && row[0]->value) ? atoll(row[0]->value) : 0;
							}

							else
							{
								amount = 0;
							}

							evbuffer_add_printf(buf, "%lld", amount);

							db_mysql_del_row(row, nc);
						}

						else
						{
							fprintf(stderr, "Query error:%s\n", query);
		
							return;
						}
					}

					else if (0 == strncasecmp(oper, "trn", 3))
					{
						// Transfer coin to target account
						// A banker can transfer to a player or a low-level banker, only postive integer allowed
						// If banker has no enough amount, all coin remained will be transfered
						// Return value is coin transfered actually

						// User type, <b> for banker and <u> for normal player
						char *utype = strtok(NULL, delimiters);
						char *uid = strtok(NULL, delimiters);
						char *value = strtok(NULL, delimiters);
						trans = atoll(value);

						if (utype && uid && value)
						{
							if (0 == strncmp(utype, "b", 1))
							{
								// Another banker
								if (atoi(uid) == session_list[session_id].bid || trans <= 0)
								{
									// You cannot transfer coin to yourself
									// Postive allowed only
									evbuffer_add_printf(buf, "TRANS_ERROR");
								}

								else
								{
									// Get banker info
									sprintf(query, "SELECT %s FROM %s WHERE %s = '%s' AND %s = '%d'", 
									    	setting.mysql_blevel_column, 
									    	setting.mysql_banker_table, 
									    	setting.mysql_bid_column, 
									    	uid, 
									    	setting.mysql_brid_column, 
									    	session_list[session_id].runner_id);

									if (0 == db_mysql_query(0, query))
									{
										row = db_mysql_fetch_row(0, &nc);

										if (nc == 1)
										{
											blevel = (row && row[0]->value) ? atoi(row[0]->value) : 0;
										}

										else
										{
											blevel = 0;
										}

										db_mysql_del_row(row, nc);
									}

									else
									{
										fprintf(stderr, "Query error:%s\n", query);

										return;
									}

									// Target level must greater(lower) than you
									if (blevel <= session_list[session_id].blevel)
									{
										evbuffer_add_printf(buf, "LEVEL_ERROR");
									}

									else
									{
										// Banker's coin amount
										sprintf(query, "SELECT %s FROM %s WHERE %s = %d", 
												setting.mysql_bamount_column, 
												setting.mysql_banker_table, 
												setting.mysql_bid_column, 
												session_list[session_id].bid);

										if (0 == db_mysql_query(0, query))
										{
											row = db_mysql_fetch_row(0, &nc);

											if (nc == 1)
											{
												amount = (row && row[0]->value) ? atoll(row[0]->value) : 0;
											}

											else
											{
												amount = 0;
											}

											evbuffer_add_printf(buf, "%lld", amount);

											db_mysql_del_row(row, nc);
										}

										else
										{
											fprintf(stderr, "Query error:%s\n", query);
		
											return;
										}

										if (amount > 0)
										{
											trans = (amount > trans) ? trans : amount;

											// If you use InnoDB or other commit-safe storage engine, you can modify these queries
											sprintf(query, "UPDATE %s SET %s = %s - %lld WHERE %s = '%d'", 
											    	setting.mysql_banker_table, 
											    	setting.mysql_bamount_column, 
											    	setting.mysql_bamount_column, 
											    	trans, 
											    	setting.mysql_bid_column, 
											    	session_list[session_id].bid);
											
											if (0 != db_mysql_query(0, query))
											{
												fprintf(stderr, "Query error:%s\n", query);

												return;
											}

											sprintf(query, "UPDATE %s SET %s = %s + %lld WHERE %s = '%s'", 
											    	setting.mysql_banker_table, 
											    	setting.mysql_bamount_column, 
											    	setting.mysql_bamount_column, 
											    	trans, 
											    	setting.mysql_bid_column, 
											    	uid);

											if (0 != db_mysql_query(0, query))
											{
												fprintf(stderr, "Query error:%s\n", query);

												return;
											}

											evbuffer_add_printf(buf, "%lld", trans);
										}

										else
										{
											evbuffer_add_printf(buf, "BANKER_EMPTY");
										}
									}
								}
							}

							else if (0 == strncmp(utype, "u", 1))
							{
								// Normal player
								// Get user info
								sprintf(query, "SELECT %s FROM %s WHERE %s = '%s' AND %s = '%d'", 
								    	setting.mysql_uid_column, 
								    	setting.mysql_user_table, 
								    	setting.mysql_uid_column, 
								    	uid, 
								    	setting.mysql_urid_column, 
								    	session_list[session_id].runner_id);

								if (0 == db_mysql_query(0, query))
								{
									row = db_mysql_fetch_row(0, &nc);

									if (nc == 1)
									{
										blevel = (row && row[0]->value) ? atoi(row[0]->value) : 0;
									}

									else
									{
										blevel = 0;
									}

									db_mysql_del_row(row, nc);
								}

								else
								{
									fprintf(stderr, "Query error:%s\n", query);

									return;
								}

								if (blevel <= 0)
								{
									evbuffer_add_printf(buf, "USER_NOT_FOUND");
								}

								else
								{
									// Banker's coin amount
									sprintf(query, "SELECT %s FROM %s WHERE %s = %d", 
											setting.mysql_bamount_column, 
											setting.mysql_banker_table, 
											setting.mysql_bid_column, 
											session_list[session_id].bid);

									if (0 == db_mysql_query(0, query))
									{
										row = db_mysql_fetch_row(0, &nc);

										if (nc == 1)
										{
											amount = (row && row[0]->value) ? atoll(row[0]->value) : 0;
										}

										else
										{
											amount = 0;
										}

										evbuffer_add_printf(buf, "%lld", amount);

										db_mysql_del_row(row, nc);
									}

									else
									{
										fprintf(stderr, "Query error:%s\n", query);

										return;
									}

									if (amount > 0)
									{
										trans = (amount > trans) ? trans : amount;

										// If you use InnoDB or other commit-safe storage engine, you can modify these queries
										sprintf(query, "UPDATE %s SET %s = %s - %lld WHERE %s = '%d'", 
												setting.mysql_banker_table, 
												setting.mysql_bamount_column, 
												setting.mysql_bamount_column, 
												trans, 
												setting.mysql_bid_column, 
												session_list[session_id].bid);
									
										if (0 != db_mysql_query(0, query))
										{
											fprintf(stderr, "Query error:%s\n", query);

											return;
										}

										sprintf(query, "UPDATE %s SET %s = %s + %lld WHERE %s = '%s'", 
												setting.mysql_user_table, 
												setting.mysql_ucoin_column, 
												setting.mysql_ucoin_column, 
												trans, 
												setting.mysql_uid_column, 
												uid);

										if (0 != db_mysql_query(0, query))
										{
											fprintf(stderr, "Query error:%s\n", query);

											return;
										}

										evbuffer_add_printf(buf, "%lld", trans);
									}

									else
									{
										evbuffer_add_printf(buf, "BANKER_EMPTY");
									}
								}
							}

							else
							{
								// Unknown
								evbuffer_add_printf(buf, "UTYPE_ERROR");
							}
						}
					}

					else
					{
						evbuffer_add_printf(buf, "OPER_ERROR");
					}
				}

				http_session_touch(session_id);
			}

			else
			{
				// Auth failed
				evbuffer_add_printf(buf, "SESSION_FAILED");
			}
		}
	}

	else
	{
		evbuffer_add_printf(buf, "<html><head><title>Comoro Coin Server</title></head><body><strong>Comoro Coin Server</strong></body></html>");
	}

	evhttp_send_reply(r, HTTP_OK, "OK", buf);
	evbuffer_free(buf);
	
	return;
}

/* Add session */
int http_session_add(int bid, int brid, int blevel, char *session_key)
{
	// Find a empty slot
	int i, id = -1;

	for (i = 0; i < session_list_size; i ++)
	{
		if (bid == session_list[i].bid || 0 == session_list[i].set_time)
		{
			id = i;
			break;
		}
	}

	if (-1 == id)
	{
		// Enlarge list
		struct http_session *tmp = realloc(session_list, 2 * session_list_size * sizeof(struct http_session));

		if (!tmp)
		{
			return RTN_MEMORY_ERROR;
		}

		memset(tmp + session_list_size * sizeof(struct http_session), 0, session_list_size * sizeof(struct http_session));
		id = session_list_size;
		session_list_size *= 2;
		session_list = tmp;
	}

	// Assign vars
	session_list[id].session_id = id;
	session_list[id].set_time = time((time_t *) NULL);
	session_list[id].bid = bid;
	session_list[id].runner_id = brid;
	session_list[id].blevel = blevel;
	memcpy(session_list[id].session_key, session_key, SESSION_KEY_LENGTH * 2);

	http_session_touch(id);

	return id;
}

/* Delete session */
int http_session_del(int session_id)
{
	if (session_id >= 0 && session_id < session_list_size && session_list[session_id].set_time)
	{
		memset(&session_list[session_id], 0, sizeof(struct http_session));
	}
	
	return RTN_SUCCESS;
}

/* Find session by given session key */
int http_session_get(char *session_key)
{
	int i;

	for (i = 0; i < session_list_size; i ++)
	{
		if (0 == memcmp(session_key, session_list[i].session_key, SESSION_KEY_LENGTH * 2))
		{
			return i;
		}
	}

	return RTN_GENERAL;
}

/* Touch session */
int http_session_touch(int session_id)
{
	if (session_id >= 0 && session_id < session_list_size && session_list[session_id].set_time)
	{
		// Set touch time
		session_list[session_id].touch_time = time((time_t *) NULL);
	}

	return RTN_SUCCESS;
}

/* Session deletion callback */
void http_session_check_cb(void *arg)
{
	unsigned long now = time((time_t *) NULL);
	int i;

	for (i = 0; i < session_list_size; i ++)
	{
		if (session_list[i].set_time && now - session_list[i].touch_time > SESSION_LIFETIME)
		{
			// Expired
			http_session_del(i);
		}
	}
	
	return;
}
